﻿#region Includes
using System;
using System.Drawing;
using System.Collections;
using System.Collections.Generic;

using OpenTK;
using OpenTK.Input;
using OpenTK.Graphics.OpenGL;

using GingerGL.Math;
using GingerGL.Input;
using GingerGL.Graphics;
using GingerGL.Graphics.Models.BSP;
using GingerGL.Graphics.Models.MD3;
#endregion

namespace GingerGL
{
    //------------------------------------------------------------//
    /// <summary>
    /// Scene Class
    /// <para>Draws All objects on screen</para>
    /// </summary>
    public class Engine
    {
        #region Variables
        private Matrix4 applied;
        #endregion
        //--------------------------------------------------------//
        #region Properties
        /// <summary>
        /// Gets OpenGL Version
        /// </summary>
        public Version Version { get; private set; }
        /// <summary>
        /// Gets TRUE if OpenGL Supports Shaders
        /// </summary>
        public bool SupportsGLSL { get { return this.Version.Major >= 2; } }
        /// <summary>
        /// Gets TRUE if OpenGL support Vertex & Index Buffers
        /// </summary>
        public bool SupportsVBO { get { return this.Version.Major > 1 || (this.Version.Major == 1 && this.Version.Minor >= 5); } }
        /// <summary>
        /// Gets/Sets Main Player
        /// </summary>
        public Entity Player { get; set; }
        /// <summary>
        /// Gets/Sets Camera Position
        /// </summary>
        public Vector3 CameraPosition { get { return this.Camera.Position; } }
        /// <summary>
        /// Gets/Sets Settings about the Render State
        /// </summary>
        public RenderState RenderState { get; set; }
        /// <summary>
        /// Gets the time between the last frame update
        /// </summary>
        public double LastFrameTime { get; private set; }
        /// <summary>
        /// Game Window Font
        /// </summary>
        public QFont Text { get; set; }
        /// <summary>
        /// Game Window
        /// </summary>
        public GameWindow GameWindow { get; private set; }
        /// <summary>
        /// Objects in Scene
        /// </summary>
        public Scene Scene { get; set; }
        /// <summary>
        /// Gets/Sets TRUE if the Scene Rendering is Paused
        /// </summary>
        public bool Paused { get; set; }
        /// <summary>
        /// Gets the current controller
        /// </summary>
        public Camera Camera { get; private set; }
        /// <summary>
        /// Gets the Direction of the Mouse
        /// </summary>
        public Ray MouseDirection { get; private set; }
        /// <summary>
        /// Gets the Current Frustum;
        /// </summary>
        public Frustum Frustum { get; set; }
        /// <summary>
        /// Global Scene Class
        /// </summary>
        static public Engine Instance;
        #endregion
        //--------------------------------------------------------//
        #region Constructor
        /// <summary>
        /// Initializes Scene
        /// </summary>
        public Engine()
        {
            #region Game Window
            //game window
            this.GameWindow = new GameWindow
            (
                800, 
                600, 
                OpenTK.Graphics.GraphicsMode.Default, 
                "Ginger 3D", 
                GameWindowFlags.Default
            );            
            this.GameWindow.VSync = VSyncMode.On;
            this.GameWindow.CursorVisible = false;
            this.GameWindow.Resize += new EventHandler<EventArgs>(OnResize);
            this.GameWindow.UpdateFrame += new EventHandler<FrameEventArgs>(OnUpdateFrame);
            this.GameWindow.RenderFrame += new EventHandler<FrameEventArgs>(OnRenderFrame);
            this.GameWindow.Load += new EventHandler<EventArgs>(OnLoad);
            this.GameWindow.Unload += new EventHandler<EventArgs>(OnUnload);
            #endregion

            #region Variables & Properties
            //version
            string VersionOpenGL = GL.GetString(StringName.Version);
            this.Version = new System.Version(int.Parse(VersionOpenGL[0].ToString()), int.Parse(VersionOpenGL[2].ToString()));
            
            //scene
            this.Scene = new DefaultScene();
            
            //render state
            this.RenderState = new RenderState();
    
            //camera
            this.Camera = new FirstPersonCamera();
            this.Camera.Set(new Vector3(0f, 0.875f, 0f));

            //frustum
            this.Frustum = new Math.Frustum();
            #endregion

            //skybox    
            this.Scene.Add(Skybox.Load (@"data\images\skybox\01\front.png", @"data\images\skybox\01\right.png", @"data\images\skybox\01\back.png", @"data\images\skybox\01\left.png", @"data\images\skybox\01\top.png", @"data\images\skybox\01\bottom.png"));
            this.Scene.Add(BSP.Load (@"data\maps\test1.bsp"));
            this.Scene.Add(GridMap.Create(50));
            //this.Text = new QFont("data/Fonts/Comfortaa-Regular.ttf", 12, FontStyle.Regular);
            //text = new Text2D(@"data\fonts\Font.bmp");

            //mShape = Shapes.Cube(20);
            

            //Ginger.IO.MD3.MD3[] model = IO.MD3.MD3.Load(@"data\models\md3-thetick.pk3");
            //GingerGL.Graphics.Models.MD3.Model[] model = GingerGL.Graphics.Models.MD3.Model.Load(@"data\models\md3-troll.pk3");
            //Ginger.IO.MD3.MD3[] model = IO.MD3.MD3.Load(@"data\models\md3-trooper.pk3");
            //GingerGL.IO.MD3.MD3[] model = IO.MD3.MD3.Load(@"data\models\md3-vadr.pk3");
            //Ginger.IO.MD3.MD3[] model = IO.MD3.MD3.Load(@"data\models\md3-sarge.pk3");
            //QuakeModel qm = new QuakeModel(model[0]);
            //qm.SetLegSequence("run");
            //qm.SetTorsoSequence("attack");
            //mObject = qm;
        }        
        #endregion
        //--------------------------------------------------------//
        #region Methods
        /// <summary>
        /// Gets a Ray based on the position of the mouse
        /// </summary>
        public Ray MousePosition(int mousex, int mousey)
        {
            int[] viewport = new int[4];
            Matrix4 m, p;
            GL.GetInteger(GetPName.Viewport, viewport);
            GL.GetFloat(GetPName.ModelviewMatrix, out m);
            GL.GetFloat(GetPName.ProjectionMatrix, out p);

            // Devince.OpenGL renders with (0,0) on height, mouse reports with (0,0) on top
            Vector3 result = UnProject(
                new Vector3(mousex, viewport[3] - mousey, 0), m, p, viewport);

            if (result != null)
            {
                Vector3 direction = new Vector3(result);
                direction -= this.CameraPosition;
                direction.Normalize();
                return new Ray(direction, this.CameraPosition);
            }
            return new Ray();
        }
        /// <summary>
        /// Game Window was resized
        /// </summary>
        private void OnResize(object sender, EventArgs e) { this.RenderState.Resize(); }
        /// <summary>
        /// Render Scene
        /// </summary>
        private void OnRenderFrame(object sender, FrameEventArgs e)
        {
            #region Scene State Checks
            if (!this.GameWindow.Focused) return;                               //does the window have focus?
            //if (this.Paused) return;                            //are we paused?
            //to-do, check entity states
            #endregion

            #region Setup Scene
            this.Begin(this, e);                                     //being to render scene
            #endregion

            #region Render Scene
            this.Render(e);
            #endregion

            #region  Finish Scene
            this.End(this, e);
            #endregion
        }
        /// <summary>
        /// Render Scene
        /// </summary>
        private void Render(FrameEventArgs e)
        {
            #region Setup World
            this.Camera.Draw();                                 //apply world matrix
            this.MouseDirection = MousePosition(                //get mouse direction
                this.GameWindow.Mouse.X, 
                this.GameWindow.Mouse.Y);
            #endregion

            if (this.Scene.Visible && this.Scene != null)       //is there a scene ready?
            {
                #region First Renderable
                if (this.Scene.FirstRenderable != null)         //does something need to be drawn first?
                {
                    if (this.Scene.FirstRenderable.Visible)     //is it visible?
                    {
                        GL.PushMatrix();                        //save state\
                        GL.Color4(0.8f, 0.8f, 0.8f, 1f);        //for good measure
                        this.Scene.FirstRenderable.Draw();      //draw object
                        GL.PopMatrix();                         //restore state
                    }
                }
                #endregion

                #region Render Objects
                foreach (IRenderable obj in Scene)              //cycle thru objects
                {
                    if (obj.Visible)                            //if visible, continue
                    {
                        GL.PushMatrix();                        //save state
                        GL.Color4(0.8f, 0.8f, 0.8f, 1f);        //for good measure
                        //GL.LoadIdentity();
                        obj.Draw();                             //draw object
                        GL.PopMatrix();                         //restore state
                    }
                }
                #endregion

                #region Last Renderable
                
                if (this.Scene.LastRenderable != null)          //does something need to be drawn first?
                {
                    if (this.Scene.LastRenderable.Visible)      //is it visible?
                    {
                        GL.PushMatrix();                        //save state
                        GL.Color4(0.8f, 0.8f, 0.8f, 1f);        //for good measure
                        this.Scene.LastRenderable.Draw();     //draw object
                        GL.PopMatrix();                         //restore state
                    }
                }
                #endregion
            }

        }
        /// <summary>
        /// Post Render Update
        /// </summary>
        private void OnUpdateFrame(object sender, FrameEventArgs e) 
        {
            KeyBindings.Instance.Update(); 
        }
        /// <summary>
        /// Initialize Scene
        /// </summary>
        private void OnLoad(object sender, EventArgs e) 
        { 
            InitializeOpenGL();
            InitializeKeyBindings();
        }
        /// <summary>
        /// Unloads Scene
        /// </summary>
        private void OnUnload(object sender, EventArgs e)
        {
            this.Scene.UnloadAll();                             //unload all scene objects
            Unmanaged.DiscardRemainingResources();              //unload all unmanaged objects
        }
        /// <summary>
        /// Begin to Render the Scene
        /// </summary>
        private void Begin(object sender, FrameEventArgs e)
        {
            GL.Clear(this.RenderState.ClearBufferMask);         //clear screen
            GL.LoadIdentity();                                  //Reset The Modelview Matrix
            this.LastFrameTime = e.Time;                        //set time for global viewing
        }
        /// <summary>
        /// End Scene
        /// </summary>
        private void End(object sender, FrameEventArgs e)
        {
            this.GameWindow.SwapBuffers();                      //swap pixel buffers
            this.GameWindow.Title = this.GameWindow.RenderFrequency.ToString();
        }
        /// <summary>
        /// Initialize the OpenGL Defaults
        /// </summary>
        private void InitializeOpenGL()
        {
            GL.ShadeModel(ShadingModel.Smooth);                                 // Enable Smooth Shading
            GL.ClearColor(this.RenderState.ClearColor);                         // Black Background
            GL.Color4(1.0f, 1.0f, 1.0f, 1.0f);                                  // White Foreground
            GL.Enable(EnableCap.DepthTest);							            // Enables Depth Testing
            GL.Enable(EnableCap.StencilTest);                                   // Enables Stencil Testing
            GL.Enable(EnableCap.Blend);                                         // Enables Blending
            GL.Enable(EnableCap.Texture2D);                                     // Enables Textures
            GL.ClearDepth(1.0f);								                // Depth Buffer Setup
            GL.DepthFunc(DepthFunction.Lequal);								    // The Type Of Depth Testing To Do
            GL.DepthRange(0.0, 1.0);
            GL.CullFace(CullFaceMode.Front);
            GL.Enable(EnableCap.CullFace);                                      // Enables Culling Front Face
            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);	    // Really Nice Perspective Calculations

            //GL.Enable(EnableCap.Lighting);
            //GL.LightModel(LightModelParameter.LightModelAmbient, new float[] { 1f, 1f, 1f, 1.0f });
            //GL.Enable(EnableCap.ColorMaterial);
            //GL.ColorMaterial(MaterialFace.Front, ColorMaterialParameter.AmbientAndDiffuse);
        }
        /// <summary>
        /// Initialize Key Bindings
        /// </summary>
        private void InitializeKeyBindings()
        {
            KeyBindings.Instance.Unbind();
            KeyBindings.Instance.Bind(Key.W, () => { this.Camera.Move(MoveDirection.Forward); });
            KeyBindings.Instance.Bind(Key.S, () => { this.Camera.Move(MoveDirection.Backward); });
            KeyBindings.Instance.Bind(Key.A, () => { this.Camera.Displace(DisplaceDirection.Left); });
            KeyBindings.Instance.Bind(Key.D, () => { this.Camera.Displace(DisplaceDirection.Right); });
            KeyBindings.Instance.Bind(Key.Q, () => { this.Camera.Elevate(ElevationDirection.Ascend); });
            KeyBindings.Instance.Bind(Key.Z, () => { this.Camera.Elevate(ElevationDirection.Descend); });
            //KeyBindings.Instance.Bind(Key.Right, () => {RotaY_Horizontal(-KeyRotationSpeed);});
            //KeyBindings.Instance.Bind(Key.Left, () => {RotaY_Horizontal(+KeyRotationSpeed);});
            //KeyBindings.Instance.Bind(Key.Up, () => { this.Rotate(RotateAxis.X, +MouseRotationSpeed); });
            //KeyBindings.Instance.Bind(Key.Down, () => { this.Rotate(RotateAxis.X, -MouseRotationSpeed); });
            KeyBindings.Instance.Bind(Key.Escape, () => { Engine.Instance.GameWindow.Exit(); });
            KeyBindings.Instance.Bind(Key.Pause, () => { Engine.Instance.Paused = !Engine.Instance.Paused; });
            KeyBindings.Instance.Bind(this.Camera.MouseMove);
        }
        /// <summary>
        /// Starts Scene
        /// </summary>
        /// <param name="fps">Frames Per Second</param>
        static public void Start(int fps)
        {
            Instance = new Engine();
            Instance.GameWindow.Run();
        }
        /// <summary>
        /// Change Projection to and Ortho 2D
        /// </summary>
        static public void Ortho2D(double left, double right, double bottom, double top)
        {
            GL.Ortho(left, right, bottom, top, -1.0, 1.0);
        }
        /// <summary>
        /// From Mesa Lib
        /// </summary>
        static public Vector3 UnProject(Vector3 window, Matrix4 modelMatrix, Matrix4 projMatrix, int[] viewport)
        {
            Matrix4 final = Matrix4.Mult(projMatrix, modelMatrix);
            final.Invert();

            if (final.Determinant == 0)
                return Vector3.Zero;

            Vector3 result = new Vector3(window);

            /* Map x1 and y1 from window coordinates */
            result.X = (result.X - viewport[0]) / viewport[2];
            result.Y = (result.Y - viewport[1]) / viewport[3];

            /* Map to range -1 to 1 */
            result.X = result.X * 2 - 1;
            result.Y = result.Y * 2 - 1;
            result.Z = result.Z * 2 - 1;

            return Vector3.TransformVector(result, final);
        }
        /// <summary>
        /// Look At
        /// </summary>
        static public void LookAt(Vector3 position, Vector3 target, Vector3 up)
        {
            Vector3 forward = position - target;
            forward.Normalize();

            // Side = Forward x1 tUp
            Vector3 right = Vector3.Cross(forward, up);
            right.Normalize();

            // Recompute tUp as: tUp = tRight x1 tForward
            up = Vector3.Cross(right, forward);

            Matrix4 m = new Matrix4(
                new Vector4(right.X, up.X, -forward.X, 0),
                new Vector4(right.Y, up.Y, -forward.Y, 0),
                new Vector4(right.Z, up.Z, -forward.Z, 0),
                new Vector4(0, 0, 0, 1));
            
            GL.MultMatrix(ref m);
            GL.Translate(-position);
        }
        #endregion
        //--------------------------------------------------------//
    }
    //------------------------------------------------------------//
}
